{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Git 与 GitHub 入门"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-time-machine.png\" width=\"700\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Git* 是一个“分布式版本控制系统”——听起来也许跟你没关系，但无论是谁，都会因为能够使用 *git* 而节约时间、提高效率。进而，如果你居然没有一个活跃的 Github 账户，那么你正在错过人类史上前所未有的共同协作时代——半点都没有夸张。同样提供 *git* 工具云服务的还有 Gitlab、Bitbucket 等等。\n",
    "\n",
    "并且，Github 很可能是地球上第一个给人们提供“用作品社交”方式的平台，你若是不能参与其中，实在是太可惜了！"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "所以我们准备了这篇关于 [git](https://git-scm.com/) 和 [GitHub](https://github.com/) 的入门教程，能够帮助从来没有使用过 *git* 甚至不知道什么是版本控制（*version control*）的人掌握所有重要的基本概念和基本操作，并对使用“分布式版本控制系统（*Distributed Version Control Sysytem, DVCS*）”的两个主流方法有所了解。如果想更系统和深入了解 *git*，最好的内容是官方教科书 [Pro Git](https://git-scm.com/book/en/v2)，有各种语言的版本，值得一读再读，务必确保理解完整。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**视频讲解**：[Git 和 GitHub 简明入门](https://www.bilibili.com/video/av73411755)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 版本控制解决什么问题"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在学习 Git 之前首先我们要了解的是：为什么我们用它，它能帮我们解决什么样的问题。\n",
    "\n",
    "所谓版本控制，就是管理同一个文件的多个不同的版本，比如：\n",
    "* 最开始有个空文件，叫做版本 0 号；\n",
    "* 我们在里面写了一些东西，定义为版本 1 号；\n",
    "* 然后又做了些修改，定义为版本 2 号；\n",
    "* 我们删除了这个文件，不存在的这个状态也是个版本，叫做版本 3 号；\n",
    "* 我们决定恢复这个文件，并且恢复到版本 1 号的内容；\n",
    "* 我们在恢复的版本 1 号的基础上又做了修改，定义为版本 4 号，注意版本 2 号和 4 号都是在 1 号基础上修改而来的；\n",
    "* 我们比较了版本 2 号和 4 号，发现有些想法是可以共存的，我们把 2 号 和 4 号版本的内容合并在一起，形成了版本 5 号。\n",
    "\n",
    "如果没有版本控制，我们要做到上面这些事情就要完全靠记忆——毫无疑问这不靠谱；或者我们手工来维护这个文件的版本，每次修改都保留之前的文件，重开一个新的，一个文件会出现好多个副本，在文件名最后用日期时间之类的标记区分开，这样倒是可行，但是太累了——这还只是一个文件，要是一个目录，下面有成百上千的子目录和文件怎么办？\n",
    "\n",
    "*Git*（以及其他版本控制系统）就是一个帮助我们管理文件版本的工具，我们只要告诉 *git* 把哪些修改记录为一个版本，它就会照做，进而能随时根据我们的需要回到某个版本；*git* 还提供了一系列命令，帮助我们比较两个版本之间的差异，或者将两个不同版本合并在一起。这就是版本控制的**基本能力**：**记录修改历史，返回某个历史状态，历史版本的查阅、比较和合并**。*Git* 对这类任务提供了非常高效的实现，哪怕是几个 GB 成千上万文件的历史记录，都可以很快速的进行操作，但 *git* 的伟大之处主要不在于此，而在于解决了多人协作环境下的版本控制问题。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "设想有多个人同时在维护一组文件，每个人都可以对文件进行修改，这个时候的版本控制除了具备前面说的“基本能力”，要解决的问题一下就多了起来，比如：\n",
    "* 同步：其他人如何知道某个人做出的修改？\n",
    "* 分支合并：两个人同时对一个文件做出了修改，如何合并他们的工作？\n",
    "* 修改隔离：如何避免一个人做出错误修改并同步给所有其他人？\n",
    "\n",
    "*Git* 提供了一种思维模型和完整配套工具来解决多人协作环境下的版本控制问题，辅以相应的组织架构和角色分工，能够搭建出适应不同目标的协作模型，正是在这种基础之上，才出现了 GitHub 这样的开源协作服务，进而促进了在线内容协同（开源软件开发只是其中一种）的大发展。下面就让我们从头开始了解 *git* 和 GitHub 如何做到这些。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Git 基本概念"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在具体的操作实例之前我们先了解一下 *git* 的一些最核心的基本概念和术语，本节所有用黑体标注的中文（对应英文用斜体标注）的都属于关键的基本概念，以后会经常遇到。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 工作目录、仓库和暂存区域"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**工作目录**（*working directory*）就是我们工作的目录，简单直白，这个目录以及下面所有的子目录和文件就是我们操作的对象，在 *git* 环境下，工作目录是我们实际操作的、文件系统里真正存在的对象。\n",
    "\n",
    "**仓库**（*repository*，经常被简称为 *repo*）是 *git* 为我们建立和维护的版本历史数据库，这个数据库躲在一个叫 `.git` 的隐藏目录下，通常我们不需要直接操作它，`git` 命令会帮助我们来操作它。\n",
    "\n",
    "**暂存区域**（*staging area*）则是工作目录和仓库之间的中间过渡区，有时也叫**索引**（*index*），不过我们觉得“暂存区域”的叫法更清晰易懂。当我们在工作目录里做了一些修改之后，可能只想把一部分修改作为一个版本提交到仓库里保存下来，这时候就可以选择几个修改先放到暂存区域，然后再把暂存区域里的修改提交到版本仓库。\n",
    "\n",
    "如果我们想**检取**（*checkout*）某个历史版本来操作，那么只要提供给版本号，*git* 就从仓库里找到对应版本然后把工作目录变成那个版本的样子，我们就可以在这个版本上工作了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-states.png\" width=\"500\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "注意，Git 将整个工作目录视为一个整体，每一个版本对应的都是这个整体的一个快照。当然 Git 做了很多优化，并不会真的存好多份整个目录的内容，所以不用担心 Git 把硬盘都浪费了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 本地仓库和远程仓库"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Git* 是第一个（也是目前应用最广泛）的“分布式”版本控制系统，这个“分布式（*distributed*）”代表了版本仓库可以在网络上方便的复制。假设 A 创建一个 *git* 版本仓库，将仓库的访问地址告诉 B，B 就可以“**克隆**（*clone*）”这个版本仓库到自己的电脑中（当然前提是网络连通且具有相应的权限），而且这两个仓库可以随时同步各自的修改变化。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "那么如果有很多人呢？通常我们会设置一台电脑作为公共仓库服务器，一个团队成员（通常是负责管理版本的人）**创建**（*init*）好仓库然后“**推送**（*push*）”到公共仓库服务器上，这个服务器上的仓库就成为团队共享的仓库（*team repo*），其他人就可以克隆 *team repo* 到自己的电脑，*team repo* 成为是所有人公共的“**源**（*origin*）”。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-local-remote-repos.png\" width=\"700\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如此这般之后，所有人都可以在自己电脑上工作，在自己的本地仓库中建立新的版本，并在需要时把本地仓库中的版本 **推送**（*push*） 到 *team repo*，也可以从 *team repo* **抓取**（*pull*） 其他人提交的版本到自己的本地仓库。如果这个过程中产生了**冲突**（*conflict*），比如两个人提交的两个版本中有对同一个文件的不相容的修改，有多种选项来进行解决。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-pull-push.png\" width=\"350\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Git 基本用法：A Tutorial"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在了解了上面的基本概念之后，下面就请跟着我们的指引一步步的操作来熟悉 *git* 的基本用法吧。*Tutorial* 这个词儿在编程领域特别常见，它的本意是“辅导课”，在我们这个领域经常代表一个入门的学习例子，带有详细的 *step-by-step* 操作指引，跟着做一遍基本就入门了，下面就是个例子。\n",
    "\n",
    "继续之前请确认已经按照[之前的指引](x1-setup.md)配置好了你的系统，即命令行界面、软件包管理工具、VSCode、*git* 都安装配置完毕。下面列出的所有命令（除特别指出外）都需要你在命令行界面中输入执行，"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 使用前的基本设置"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在使用之前需要对 *git* 进行一些基本设置。\n",
    "\n",
    "首先是设置你的名字和 Email，这些信息会出现在你提交的每一个版本的版本记录中（注意 `git config` 后面的参数带有两个横线 `--`）：\n",
    "\n",
    "```shell\n",
    "git config --global user.name \"your name\"\n",
    "git config --global user.email your_email@foo.bar\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然后是配置 *git* 使用的文本编辑器，*git* 的某些操作需要打开一个文本编辑器让你输入一些东西，比如在提交版本时必须输入的版本描述，如果不做设置 *git* 会打开系统里有的 [Vim](https://www.vim.org/) 或者 [Emacs](https://www.gnu.org/software/emacs/) 编辑器，这两个都是历史悠久功能强大的通用编辑器，但是对没用过的人来说都有些门槛，所以我们建议一开始就设置让 *git* 使用我们之前安装好的 VSCode：\n",
    "\n",
    "```shell\n",
    "git config --global core.editor \"code -w\"\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以用下面的命令来查看当前配置，可能有不少你不知道干啥的配置，没关系，现在不用管，确认刚才我们设置的三项是对的就行了：\n",
    "\n",
    "```shell\n",
    "git config --list\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 基本操作"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "打开命令行界面，你目前应该处在用户根目录下，进入我们之前创建的 `Code` 子目录，在其下创建一个新的子目录用于下面整个例子：\n",
    "\n",
    "```shell\n",
    "cd ~/Code\n",
    "mkdir learn-git\n",
    "cd learn-git\n",
    "pwd\n",
    "```\n",
    "\n",
    "最后一个 `pwd` 命令会打印出你当前所在目录供确认，这就是我们的**工作目录**（*working directory*），里面还没有文件。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们可以用下面的命令来初始化仓库：\n",
    "\n",
    "```shell\n",
    "git init\n",
    "```\n",
    "\n",
    "这个命令成功后会提示已经在当前目录下的 `.git` 子目录下初始化了一个空的 *repo*，这时候如果我们输入 `ls` 来查看是看不到这个 `.git` 目录的，因为 `.` 打头的目录是隐藏的，我们可以用 `ls -la` 来显示它（Windows PowerShell 下请用 `ls -Force`）。\n",
    "\n",
    "这个 `.git` 目录下就是我们的版本仓库，使用 *git* 的特定数据格式保存。这也就意味着，我们的工作目录已经成为一个“由 *git* 进行版本控制”的目录，我们在这里做的任何事情都会被 *git* 跟踪，也可以作为版本提交到 *repo* 中去。\n",
    "\n",
    "```shell\n",
    "git status\n",
    "```\n",
    "\n",
    "上面的命令将显示目前工作目录的状态，比如有没有文件被修改，目前还什么都没有。这个命令对我们初学者很有用，我们应该养成习惯经常执行一下，看看在 *git* 的视角发生了什么。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在让我们创建一些文件来试试。\n",
    "\n",
    "```shell\n",
    "touch README.md\n",
    "code README.md\n",
    "```\n",
    "\n",
    "第一行的 `touch` 命令创建一个空的名为 `README.md` 的文件，下一行则是用 VSCode 打开这个空的文件，你可以随意输入一点什么，保存然后回到命令行界面。\n",
    "\n",
    "再次运行 `git status` 可以看到下面的提示：\n",
    "\n",
    "```shell\n",
    "On branch master\n",
    "\n",
    "No commits yet\n",
    "\n",
    "Untracked files:\n",
    "  (use \"git add <file>...\" to include in what will be committed)\n",
    "\n",
    "\tREADME.md\n",
    "\n",
    "nothing added to commit but untracked files present (use \"git add\" to track)\n",
    "```\n",
    "\n",
    "这些信息的含义是：\n",
    "* `On branch master`：当前工作在 master 分支下，关于分支我们后面会讲，现在先跳过；\n",
    "* `No commits yet`：目前版本仓库里还没有任何版本记录，Git 中的 *commit* 就是版本的概念；\n",
    "* `Untracked files:` 以及后面的文件列表：列出了工作目录下存在但还没有加入版本仓库的文件，并且提示可以用 `git add` 来将文件加入到版本仓库中；\n",
    "\n",
    "最后一行是对当前状态的综述，告诉我们还没有准备提交的文件变化，但有些新文件存在了，可以用 `git add` 来将文件加入版本仓库。我们照做：\n",
    "\n",
    "```shell\n",
    "git add README.md\n",
    "```\n",
    "\n",
    "> `git add` 这个命令一般用来把有变化的文件加入待提交的“暂存区域（*staging area*）”，但对 *untracked files* 它具有双重功效，先将其加入到版本管理中（从 *untracked* 变成 *tracked*），再加入 *staging area*。\n",
    "\n",
    "然后再运行 `git status` 可以看到下面的提示：\n",
    "\n",
    "```shell\n",
    "On branch master\n",
    "\n",
    "No commits yet\n",
    "\n",
    "Changes to be committed:\n",
    "  (use \"git rm --cached <file>...\" to unstage)\n",
    "\n",
    "\tnew file:   README.md\n",
    "```\n",
    "\n",
    "这次就没有 `Untracked files` 了，因为 `README.md` 这个文件已经放到了 `Changes to be committed` 列表下，表示这个文件的变化已经准备好**提交**（*commit*），其实就是表示它被我们放到了**暂存区域**（*staging area*）。\n",
    "\n",
    "下面还有一行提示，告诉我们如果想把文件“移出暂存区域（*unstage*）”，可以用 `git rm --cached <file>` 这个命令，我们后面会试试这个命令，现在我们先赶紧完成第一次 *commit*：\n",
    "\n",
    "```shell\n",
    "git commit\n",
    "```\n",
    "\n",
    "这个命令会 commit 目前在 *staging area* 的任何文件变化到版本仓库，在实际执行前会打开文本编辑器让我们输入 *commit message* 也就是版本说明："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-commit-message.png\" width=\"700\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在提示光标处输入我们对这个版本的描述信息，保存并关闭编辑器窗口之后 *git* 才会执行 *commit* 指令。版本信息非常重要，应该准确、扼要的概括我们所做的修改，这样后续我们管理历史版本时会少很多麻烦。\n",
    "\n",
    "> 另外选择一次提交哪些修改（事实上可以指定到具体某一行）也很有学问，一般来说，为一个明确而单一的目标所做的修改应该放在一次 *commit* 中，这也是我们需要慢慢去体会的一个重要原则。\n",
    "\n",
    "回到命令行可以看到执行结果：\n",
    "\n",
    "```shell\n",
    "[master (root-commit) 7451a00] Init commit\n",
    " 1 file changed, 3 insertions(+)\n",
    " create mode 100644 README.md\n",
    "```\n",
    "\n",
    "这些信息告诉我们：\n",
    "* master 分支下创建了一个新的 *commit*（可以理解为版本，下面不再注释），因为是整个版本仓库的第一个，所以标记为 `root-commit`；\n",
    "* 这个 *commit* 的编号（*commit id*）为 `7451a00`，这是一个全局唯一的十六进制数（你那里应该是别的数字），以后我们可以用这个编号来管理这个 *commit*；\n",
    "* 然后是我们输入的 *commit message*；\n",
    "* 最后是这次 *commit* 的一些 summary 信息（涉及一个文件变化，三个新的行被插入到版本仓库中等等）。\n",
    "\n",
    "现在执行 `git status` 可以看到：\n",
    "\n",
    "```shell\n",
    "On branch master\n",
    "nothing to commit, working tree clean\n",
    "```\n",
    "\n",
    "也就是说我们刚提交完，工作目录和仓库里的最新 *commit* 是完全一样的，没有什么可以提交的新变化。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> 每一次 *commit*，*git* 都会在 *repo* 中创建工作目录的一个完整的**快照**（*snapshot*），相当于记录了这个 *commit* 对应的工作目录的完整状态（但不包括未放进**暂存区域**（*staging area*）的那些修改），也就是说这个快照是在上一个快照基础上加上这一次 *commit* 中所有修改得到的，而唯一的 *commit id* 就指向这个状态，以后我们可以用这个 *id* 来恢复到这个状态，或者拿来和别的状态进行比较等等。\n",
    "\n",
    "下面我们来多做些操作，多 *commit* 几次试试。\n",
    "\n",
    "首先在命令行下运行 `code .` 用 VSCode 打开当前目录，VSCode 会列出当前目录下所有文件（也就是刚才创建的 `README.md`），我们可以在 VSCode 里新建几个文件，里面填入一些内容，比如可以创建两个 Python 的程序源文件（很快我们就知道这些程序是啥意思以及怎么写了）：\n",
    "\n",
    "hello.py:\n",
    "```python\n",
    "print('Hello world!')\n",
    "```\n",
    "\n",
    "today.py:\n",
    "```python\n",
    "from datetime import datetime\n",
    "print(datetime.now().strftime('今天是%Y年%m月%d日'))\n",
    "```\n",
    "\n",
    "同时我们也可以改一下之前有的 `README.md` 文件，随便改点什么都可以，然后回到命令行界面运行 `git status` 可以看到类似下面的信息：\n",
    "\n",
    "```shell\n",
    "On branch master\n",
    "Changes not staged for commit:\n",
    "  (use \"git add <file>...\" to update what will be committed)\n",
    "  (use \"git checkout -- <file>...\" to discard changes in working directory)\n",
    "\n",
    "\tmodified:   README.md\n",
    "\n",
    "Untracked files:\n",
    "  (use \"git add <file>...\" to include in what will be committed)\n",
    "\n",
    "\thello.py\n",
    "\ttoday.py\n",
    "\n",
    "no changes added to commit (use \"git add\" and/or \"git commit -a\")\n",
    "```\n",
    "\n",
    "第一段 `Changes not staged for commit:` 列出的是已经加入版本仓库但有了新变化的文件（`README.md`），我们可以像前面一样用 `git add` 来将新的变化加入 *staging area*，然后再 *commit*。\n",
    "\n",
    "> 如果我们发现改动是误操作，希望恢复到该文件上一个 commit 的状态可以使用这个命令：`git checkout -- README.md`。\n",
    "> \n",
    "> `git checkout` 从版本仓库中取得一个文件的指定版本，然后覆盖到工作目录中。命令后面可以指定一个 *commit id*，上面我们用 `--` 就表示从仓库里最新的一个 *commit* 中取，最后是要取出的文件路径（当前目录下 `README.md` 文件）。运行上面的命令会废弃掉（*discard*）刚才对 `README.md` 做的修改，我们目前并不需要这么做。\n",
    "\n",
    "第二段 `Untracked files:` 和之前没什么区别，列出了新出现还未加入版本仓库的文件。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们可以把所有变化都加入 *staging area*：\n",
    "\n",
    "```shell\n",
    "git add .\n",
    "```\n",
    "\n",
    "这个命令中的 `.` 表示将当前目录及其下面变化的所有文件都加入到 *staging area*，完成后看 `git status`：\n",
    "\n",
    "```shell\n",
    "On branch master\n",
    "Changes to be committed:\n",
    "  (use \"git reset HEAD <file>...\" to unstage)\n",
    "\n",
    "\tmodified:   README.md\n",
    "\tnew file:   hello.py\n",
    "\tnew file:   today.py\n",
    "```\n",
    "\n",
    "我们可以看到有三个文件，两个新的，一个修改的，放在 *staging area* 等待提交。这个时候我们考虑到 `README.md` 的修改和添加的两个新文件是两件事，最好分开提交，所以我们想先把第一项从 *staging area* 移出来，这时候就用到 `git reset` 命令了：\n",
    "\n",
    "```shell\n",
    "git reset HEAD README.md\n",
    "```\n",
    "\n",
    "注意，`git reset` 是个颇为复杂的命令，要把它完全搞清楚远远超出了这个小小的 *tutorial* 的篇幅，有兴趣的话可以看看 [Git from the bottom up](https://jwiegley.github.io/git-from-the-bottom-up/) 中的相关介绍（最好整篇都读）。\n",
    "\n",
    "再看 `git status`：\n",
    "\n",
    "```shell\n",
    "On branch master\n",
    "Changes to be committed:\n",
    "  (use \"git reset HEAD <file>...\" to unstage)\n",
    "\n",
    "\tnew file:   hello.py\n",
    "\tnew file:   today.py\n",
    "\n",
    "Changes not staged for commit:\n",
    "  (use \"git add <file>...\" to update what will be committed)\n",
    "  (use \"git checkout -- <file>...\" to discard changes in working directory)\n",
    "\n",
    "\tmodified:   README.md\n",
    "```\n",
    "\n",
    "应我们的要求 `README.md` 已经变成了 not staged 的状态，现在我们可以运行 `git commit` 命令来提交那两个新加的 Python 文件，输入 *commit message* 之后提交成功。\n",
    "\n",
    "然后我们可以再来提交 `README.md` 的修改了：\n",
    "\n",
    "```shell\n",
    "git add README.md\n",
    "git commit\n",
    "```\n",
    "\n",
    "这些操作的每一步之后都记得运行 `git status` 看看状态。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 版本历史"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们已经进行了三次版本提交，现在我们可以来看看版本的历史记录了：\n",
    "\n",
    "```shell\n",
    "git log\n",
    "```\n",
    "\n",
    "结果大致这样："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-log.png\" width=\"600\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`git log` 命令列出了版本仓库中已有的 *commit history*，每一个 *commit* 一段，从新到旧排列（最后的 *commit* 在最上面）。最新的 *commit* 后面有个 `HEAD -> master` 的标志，这个和分支有关，我们后面会讲。\n",
    "\n",
    "每一段开头都是 `commit` 字样后面跟着 *commit id*，非常长，但我们一般可以用前几位来代替；之后是提交作者、提交日期时间，然后是 *commit message*。信息简明扼要、一目了然，谁什么时候做了什么都被清晰记录在案，真是非常合适的“**工作证明**（*Proof of Work*）”。\n",
    "\n",
    "按 `q` 键退回到命令行界面。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们可以回到上面列出的任何一个 *commit* 的状态，我们来试试：\n",
    "\n",
    "```shell\n",
    "git checkout 7451a00\n",
    "```\n",
    "\n",
    "会有一大段提示说明文字，提到什么 `HEAD` 之类的奇怪概念，先不管它（后面到分支一节会讲），看看你的工作目录，是不是回到了你最初提交 `README.md` 一个文件的状态？新加入的文件和对 `README.md` 的修改都不见了，仿佛是进行了时间旅行一般。不用担心，时间旅行的只是你的 *working directory*，所有的修改记录都还在 *repo* 里，现在可以用下面的命令回到当前：\n",
    "\n",
    "```shell\n",
    "git checkout master\n",
    "```\n",
    "\n",
    "你会发现一切又回到了最新的状态（再一次，我们会在关于分支的一节讲这个 master 是啥）。\n",
    "\n",
    "我们还可以比较两个历史状态之间的差别，试试下面的命令：\n",
    "\n",
    "```shell\n",
    "git diff 7451a00\n",
    "git diff 7451a00 5d4f3df\n",
    "```\n",
    "\n",
    "前一行命令比较指定 *commit* 和工作目录当前状态，第二行命令比较两个指定的 *commit*。输出的内容是一种专门的 *diff format*，是 Unix 系统做文件比较的标准输出格式，其中列出了有差异的文件，以及这些文件中有差异的行，列出了哪些行是新增，哪些行被删除，那些行有变化。对于现在的你来说这个格式可能有点难懂，没有关系，以后会熟悉起来的，而且 VSCode 提供了图形化的界面来做文件比较和合并，很多时候也更好用一些。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-diff.png\" width=\"600\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个简单的 *tutorial* 就到此结束，下面我们介绍非常重要的**分支**（*branch*）概念及其在协同中的用法，还有如何利用 GitHub 来简化分享与协同。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 附注：关于 git 新版本的变化"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在 *git* 2.23 及以后的版本中引入了两个实验性的新命令：`git switch` 和 `git restore`，其目的是以后替代语焉不详的 `git checkout` 命令，这些新的命令（尤其是 `git restore`）从 2.23 版本开始出现在交互提示中：代替 `git reset HEAD <file>...` 用于从 *staging area* 撤出修改，以及代替 `git checkout -- <file>...` 用于回退修改。\n",
    "\n",
    "所以如果你已经更新到 2.23 或者更新的版本，可能看到的信息提示会与这个教程中不一样，但没关系，因为：\n",
    "1. 老的命令在相当长时间内都还有效；\n",
    "2. 你完全可以照新版提示操作，提前熟悉未来的命令——只要你理解它是什么意思就行，尝试和探索本来就是学习的重要过程。\n",
    "\n",
    "关于 2.23 引入的变化，`git` 官网有一篇[详细的说明](https://github.blog/2019-08-16-highlights-from-git-2-23/)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 分支和协同"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "前面说过，*git* 和 GitHub 最伟大之处在于构建了一个完善的多人在线协同环境，而要构建这样的环境，使用 *git* 时就需要遵循一定的规则，而不是随意乱用，*git* 本身提供了足够的工具来帮助我们实现协同，如果不去正确的使用那只能怪我们自己。**标签**（*tag*）和**分支**（*branch*）都是 *git* 提供的重要工具（即使不是多人协同环境下也很有用），我们先从理解这两个概念开始。\n",
    "\n",
    "说简单也简单，*tag* 和 *branch* 其实都是指向 *commit* 的指针，区别在于 *tag* 指向一个固定的 *commit*，而 *branch* 会随着你的操作始终跟着最新的 *commit*。我们先来看看 *tag*。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`git tag` 命令处理与 *tag* 有关的操作。如果不带任何参数执行 `git tag` 会输出仓库中存在的所有 *tag*，如果我们想在目前的最新的 *commit* 上打一个标签 v1.0，表示这是我们的 1.0 版本的状态，可以这么做：\n",
    "\n",
    "```shell\n",
    "git tag v1.0\n",
    "```\n",
    "\n",
    "这时再执行 `git log` 可以看到最新的 *commit* 后面出现了 `tag: v1.0` 的字样，表示在这个 *commit* 上打上了一个名叫 “v1.0” 的标签，以后我们就可以用这个标签来引用这个 *commit*，而不需要用又长有没有实际意义的 *commit id*。\n",
    "\n",
    "这种相当于 *commit* 别名的 *tag* 叫“轻量标签（*lightweight tag*）”，*Git* 还支持一种带标注的标签（*annotated tag*），这里就不详细介绍了，本质差不多，只是增加了标签本身的属性，比如是谁打的，什么时候打的等等。不管是哪种标签，我们都可以将其用在需要指定一个 *commit* 的场合，比如 *checkout*、*diff* 等操作中。\n",
    "\n",
    "我们还可以给某个更早的 *commit* 打标签，在标签名后面加上 *commit id* 即可，比如：\n",
    "\n",
    "```shell\n",
    "git tag v0.9 5d4f3df\n",
    "```\n",
    "\n",
    "如果要删除某个标签用 `git tag -d tag_name` 就可以了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**标签**（*tag*）给一个 *commit* 打上标记，帮助我们记忆，方便我们使用，而**分支**（*branch*）更进一步，能帮助我们标记一串 *commit*，是并行开发（或者叫并行创作）的必备工具。\n",
    "\n",
    "并行开发就是要做的事有不止一条线索，有的人在做一条线，有的人在做另一条线；或者一个人的一段时间在做一条线，另一段时间在做另一条线。\n",
    "\n",
    "举个例子来说，一个产品的 v1.0 版本上线之后，开始继续开发 v2.0 的新功能，这是一条线；当发现 v1.0 的一些错误时（不论自己发现还是用户报告），就需要立刻处理立刻修复，这时候不可能在新功能开发的基础上去修问题，而是要在 v1.0 那个版本基础上去修复，修复好的版本 v1.0.1 发布到生产环境上。这样新功能开发和老功能修错互不影响，到合适时候把 v1.0.1 的修改合并到 v2.0 的代码中就好了。这个逻辑大致上就像这样："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-branches-1.png\" width=\"800\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "分支是在比较复杂的工作任务中一定会出现的需求，事实上 *git* 以前的版本控制工具也都支持分支，但是它们建立分支的代价很高，要占用很多存储空间，执行也很慢，而 *git* 使用了一种非常聪明的方法来处理分支，使得分支变得很轻，可以随心所欲的使用分支，事实上 *git* 的理念是高度鼓励使用分支的，凡要做点新事情都可以开分支，在不影响其他人和其他任务的情况下用一个独立的分支去试，试好了再合并回主分支就好了，那么 *git* 是怎么做到的呢？"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在 *git* 中，*branch* 和 *tag* 一样，都是指向 *commit* 的指针，区别在于 *tag* 是固定的，而 *branch* 是会移动的。\n",
    "\n",
    "每个 *git* 版本仓库创建时都会创建一个 *branch* 叫 `master`，即主分支，在我们创建其他 *branch* 之前，这个主分支会跟着我们提交的 *commit* 走，永远指向最新的一个 *commit*。\n",
    "\n",
    "现在假定进展到某一个时刻，在主分支之外需要一个另外的分支来修某个特定的 bug，于是创建了一个新分支叫 `bug/fix1`，这时候实际上就有两个 *branch* 指针指向同一个 *commit*，那么问题就来了，我们下一次提交 *commit* 之后，这两个指针会怎么走？都跟着最新 *commit* 走吗？这么“亦步亦趋”还叫什么分支呢？\n",
    "\n",
    "这时候那个前面露过一面的 `HEAD` 就出现了，这也是一个指针，不过这是指向一个 *branch* 的指针，`HEAD` 指向哪个 *branch*，哪个 *branch* 就会跟着我们的最新 *commit* 走。\n",
    "\n",
    "假定我们当前最新 *commit* 为 `51b61b3`，此时我们创建新分支 `bug/fix1`：\n",
    "\n",
    "```shell\n",
    "git branch bug/fix1\n",
    "```\n",
    "\n",
    "`git branch` 在当前 *commit* 上创建分支，注意创建分支并不会自动切换到新创建的分支，目前我们还工作在 `master` 分支，`HEAD` 指针指向 `master` 分支，这时候 repo 的状态是这样的："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-branches-2.png\" width=\"500\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "由于我们现在需要尽快修复 bug，所以我们需要切换到新创建的 `bug/fix1` 分支，这需要用 `checkout` 命令：\n",
    "\n",
    "```shell\n",
    "git checkout bug/fix1\n",
    "```\n",
    "\n",
    "切换分支其实就是让 `HEAD` 指针指向该分支："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-branches-3.png\" width=\"500\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这时候 `HEAD` 指向的 `bug/fix1` 分支指针就会跟着我们提交的新 *commit* 走了，比如我们提交了新的 `f843665` 号 *commit*，就会变成这样："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-branches-4.png\" width=\"500\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "假定 `f843665` 号 *commit* 成功修复了 bug，我们在 `bug/fix1` 分支上的工作告一段落，需要切回主分支：\n",
    "\n",
    "```shell\n",
    "git checkout master\n",
    "```\n",
    "\n",
    "于是 `HEAD` 指针指回 `master` 分支："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-branches-5.png\" width=\"500\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在轮到 `master` 分支指针跟着我们提交的新 *commit* 走了："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/git-branches-6.png\" width=\"800\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果是多人协同的情况，上述两个分支上的工作甚至可以同时进行，只要一个人在自己机器上让 `HEAD` 指向 `master` 分支，TA就工作在 `master` 分支上；另一个人让 `HEAD` 指向 `bug/fix1` 分支，TA就工作在 `bug/fix1` 分支上；两人的工作都可以 *push* 回 *team repo*，互不影响。\n",
    "\n",
    "在合适的时间，团队里某位成员可以进行分支合并，假定是把 `bug/fix1` 分支合并到 `master` 分支（实际上可以把任意分支合并到任意分支），那么大致的流程如下：\n",
    "\n",
    "* 从 *team repo* 同步所有 *branch* 及 *commit*：`git fetch origin`\n",
    "* 确认工作在 `master` 分支下：`git checkout master`\n",
    "* 将 `bug/fix1` 分支合并到 `master` 分支：`git merge bug/fix1`\n",
    "\n",
    "合并中可能出现冲突等问题，需要手工解决，就不在这里展开细说了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这就是 *git* 的多分支工作模型，这个模型既可以用于单人工作，也可以用于多人协作，在多人协作时更具价值，让多人并行工作成为顺理成章、几乎没有额外代价的事情。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## GitHub 简介"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[GitHub](https://github.com) 的核心是提供 *git* 仓库的托管服务，也就是说你可以把版本仓库建在 GitHub 的服务器上，让全世界都能访问到，当然你自己的机器上也会有一份，你可以把你的新创作 *push* 到 GitHub，也可以从 GitHub 上 *pull* 回别人的贡献。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用 GitHub 最基础的场景有两个：\n",
    "\n",
    "* 其一是在 GitHub 创建一个新的 *repo*，然后把你在自己本地已经建好的 *repo* 放上去，这个很简单，在你的 GitHub 主页选择 “New”，然后照着页面说明一路做下去就行了；\n",
    "* 其二是把 GitHub 上已有的 *repo* 克隆到自己本地，可能还会对这个 *repo* 做出一些内容贡献，这个更简单，在你看中的 *repo* 首页按那个绿色的 “Clone or download” 按钮，拷贝下面文字框里的 *repo* 地址，然后在你的命令行界面贴到 `git clone ` 命令后面执行就行了。\n",
    "\n",
    "无论哪种场景，最终的情况都一样，就是你的本地有一个仓库对应到 GitHub 上某个仓库，你可以在本地工作，并通过 GitHub 与这个世界上任何人进行合作，这真是个壮观的大合作！"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "合作时自然会用到前一节提到的分支模型，GitHub 上的用户太多了，很多参与协作的人都互不认识，所以 GitHub 专门设计了一套标准流程来简化和规范大家的协同，这个流程就叫 “[GitHub Flow](https://help.github.com/en/articles/github-flow)”，下面对此做个简单的介绍。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "GitHub Flow 包含以下的几个步骤：\n",
    "\n",
    "1. 创建属于你的分支：可以直接在 GitHub 上操作，也可以 *clone* 回本地在本地创建，然后在 *push* 回去；需要时就创建，多少都可以，只要合理；\n",
    "2. 在且只在你自己的分支中工作和提交 *commit*；\n",
    "3. 当一个分支上的工作告一段落，可以发起一个 *pull request*，具体来说，就是 GitHub 会对你的工作分支和某个分支（比如 `master`）进行比较，给出差异列表，你可以选择全部或者部分差异打包成一个 *pull request*，*pull request* 提交之后这个 *repo* 的所有人都会看到，大家可以对这个 *pull request* 的内容进行讨论，做代码检查（*code review*），必要时还会进行测试；\n",
    "4. 经过某种机制（比如团队全员或者一个决策小组投票）这个 *pull request* 被认可，那么有权限的管理员可以在 GitHub 上执行一个**合并**（*merge*）操作，这个 *pull request* 的内容就会合并到主要分支（比如 `master`）。\n",
    "\n",
    "这个流程最大的特点就是“敞开分支但谨慎合并”，这无疑是非常适合开源内容协作的准则。\n",
    "\n",
    "同时 GitHub 将整个流程产品化，做得尽可能方便易用，在过去几年 GitHub Flow 已成为许多团队和个人事实上的标准工作流程，很多企业内的开发活动虽然不适用 GitHub，也会基于 *git* 使用与此类似的工作流程。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "另一个 GitHub 的特色是仓库的 *fork*，实际上就是在 GitHub 的服务器上做了一个 *repo* 的 *clone*，区别在于：\n",
    "\n",
    "* 不能直接往原 *repo* *push* *commits*；\n",
    "* 改用 *pull request* 来向原 *repo* 提交 *commit*。\n",
    "\n",
    "这两个差异，实际上就是强制执行 GitHub Flow，还是贯彻“敞开分支但谨慎合并”的原则。设想一个人或者一个组织把自己的重要成果开放出来，通常的合理策略是：\n",
    "\n",
    "1. 任何人都可以看到成果；\n",
    "2. 有一个内部或者核心团队来维护这些成果，不断增加新东西，优化老东西；\n",
    "3. 外部有能力的人可能也想做出贡献，但必须经过审核才能真正进入这个 *repo*。\n",
    "\n",
    "相应的 GitHub 都提供了很好的解决方案：\n",
    "\n",
    "1. 任何人都可以 *clone* 这个 *repo*；\n",
    "2. 有权限的人才能将自己的 *commit* *push* 到这个 *repo*；\n",
    "3. 任何人都可以 *fork* 这个 *repo*，在自己 *fork* 出来的那个 *repo* 里可以随便怎么玩，但要提交回原 *repo* 必须通过 *pull request* 经过审核才行。\n",
    "\n",
    "除此以外，GitHub 还会统计一个 *repo* 有多少个 *fork*，收到多少 *pull request*，又成功合并了多少 *pull request*，这些已经成为受欢迎程度的指数，也无形中鼓励了高质量的成功分享，同时让更多有才能的人更容易找到高质量的项目，并为之做出贡献。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 进阶学习资料"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "网上有不少关于 Git 和 GitHub 的优质学习资料，除了前面提到的官方教科书 [Pro Git](https://git-scm.com/book/en/v2) 以外，我们比较喜欢的还有这些：\n",
    "\n",
    "* [Git from the bottom up](https://jwiegley.github.io/git-from-the-bottom-up/)\n",
    "* [Git How To](https://githowto.com/)\n",
    "* [Git pretty - Solve Git Mess](http://justinhileman.info/article/git-pretty/)\n",
    "\n",
    "颇受欢迎的内容推荐列表 [awesome-git](https://github.com/dictcp/awesome-git) 中还有很多不错的资料，也可以参考。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
